Skip to content

fix(es/minifier): preserve args at call sites of destructured bindings#11841

Closed
jacobparis wants to merge 1 commit intoswc-project:mainfrom
jacobparis:fix/destructure-decl-stale-arity-11829
Closed

fix(es/minifier): preserve args at call sites of destructured bindings#11841
jacobparis wants to merge 1 commit intoswc-project:mainfrom
jacobparis:fix/destructure-decl-stale-arity-11829

Conversation

@jacobparis
Copy link
Copy Markdown

@jacobparis jacobparis commented May 4, 2026

I ran into this issue using Slate, getting different results during next dev and next build/start, traced back to this issue. In Slate this surfaces by breaking Editor.nodes(editor, { match: <fn> }) between next dev/prod whenever a function predicate is passed to match.

The core issue is that destructured bindings which are later assigned to a function have a param_count of 0 rather than unknown.

Description:

visit_var_decl only records param_count for let x = init style declarators. Destructuring patterns (let { x } = init, let [x] = init, etc.) fall outside the existing if let (Pat::Ident(var), Some(init)) = ... guard with no fallback, so x.param_count stays at the default None. A later assignment to a known-arity arrow then sets param_count via the merge rule (None, Known(N)) => Some(Known(N)), and ignore_unused_args_of_call (added in #11645) trims arguments at every call site of x accordingly.

Minimal repro:

function run(opts) {
    let { match } = opts;
    if (!match) match = () => true;
    return match(\"hello\", \"world\");
}

console.log(run({ match: (a, b) => a + \" \" + b }));
  • Source result: \"hello world\"
  • Minified result on current main: \"undefined undefined\" (call site becomes match())

Fix:

Mirror what report_assign_pat already does for runtime destructuring assignments: when the binding is anything other than a plain Pat::Ident, mark every bound identifier as having an unknown param count.

} else if !matches!(&decl.name, Pat::Ident(_)) {
    // Destructuring binding (object/array/rest pattern). The
    // bound identifiers are read from runtime properties or
    // elements, so we cannot statically determine the arity
    // of any callable that reaches them.
    for id in find_pat_ids::<_, Id>(&decl.name) {
        self.data
            .var_or_default(id)
            .store_param_count(Value::Unknown);
    }
}

Tests:

  • New fixture at tests/fixture/issues/11829/destructure-decl-stale-arity/ exercises the exact pattern above with the same config.json used by the feat(es/minifier): Remove useless arguments for non inlined callee #11645 fixtures (unused: true, keep_fargs: true).
  • The new test FAILS on main with diff match(\"hello\", \"world\")match() and PASSES with this fix applied.
  • All 10 sibling tests under tests/fixture/issues/11645/ still pass — including logical-and-assign-stale-arity, child-scope-reassign-merge, for-of-rebind-arity and the eval/with-stmt safety tests.
  • Full `cargo test --test compress`: 2855 passed; 0 failed; 27 ignored.

BREAKING CHANGE:

None.

Related issue (if exists):

Closes #11829.

`visit_var_decl` only records `param_count` for `let x = init` style
declarators. Destructuring patterns (`let { x } = init`, `let [x] = init`,
etc.) are skipped, so `x.param_count` stays at the default `None`. A later
assignment to a known-arity arrow (e.g. `if (!x) x = () => true`) then sets
`param_count` to `Some(Known(0))` via the merge rule
`(None, Known(0)) => Some(Known(0))`, and `ignore_unused_args_of_call`
trims arguments at every call site of `x` accordingly.

Mirror what `report_assign_pat` already does for runtime destructuring
assignments: when the binding is anything other than a plain `Pat::Ident`,
mark every bound identifier as having an unknown param count.

Closes swc-project#11829.
@jacobparis jacobparis requested review from a team as code owners May 4, 2026 00:34
Copilot AI review requested due to automatic review settings May 4, 2026 00:34
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 4, 2026

🦋 Changeset detected

Latest commit: b375384

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 4, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes an unsound arity-inference case in the minifier’s usage analyzer where destructured let/const bindings could incorrectly inherit a later “known” function arity and cause unused to trim arguments at call sites.

Changes:

  • Mark all identifiers bound via destructuring variable declarators as having param_count = Unknown in visit_var_decl.
  • Add a regression fixture for issue #11829 verifying call arguments are preserved for destructured bindings under unused + keep_fargs.
  • Add a changeset bumping swc_core and swc_ecma_minifier with a patch release note.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
crates/swc_ecma_minifier/src/usage_analyzer/analyzer/mod.rs Conservatively sets param_count to Unknown for all ids produced by destructuring declarators, preventing stale known-arity merges that lead to argument dropping.
crates/swc_ecma_minifier/tests/fixture/issues/11829/destructure-decl-stale-arity/input.js Adds minimal repro input using destructuring + conditional fallback assignment.
crates/swc_ecma_minifier/tests/fixture/issues/11829/destructure-decl-stale-arity/output.js Asserts the output preserves match("hello", "world") (no arg trimming).
crates/swc_ecma_minifier/tests/fixture/issues/11829/config.json Provides fixture config aligned with the #11645 unused-args fixtures (unused: true, keep_fargs: true).
.changeset/swc-args-destructure-fix.md Documents the patch change for release notes/versioning.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 4, 2026

Binary Sizes

File Size
swc.linux-x64-gnu.node 27M (27760712 bytes)

Commit: 8f6147c

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 4, 2026

Merging this PR will not alter performance

✅ 219 untouched benchmarks
⏩ 31 skipped benchmarks1


Comparing jacobparis:fix/destructure-decl-stale-arity-11829 (b375384) with main (9101c71)

Open in CodSpeed

Footnotes

  1. 31 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@jacobparis jacobparis closed this May 4, 2026
@github-actions github-actions Bot added this to the Planned milestone May 4, 2026
@jacobparis jacobparis reopened this May 4, 2026
@jacobparis jacobparis closed this May 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Minified function call omits argument(s)

3 participants